package com.specificgroup.jira.plugin.acpro.service.impl;

import com.atlassian.jira.bc.project.component.ProjectComponent;
import com.atlassian.jira.bc.project.component.ProjectComponentManager;
import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.exception.CreateException;
import com.atlassian.jira.external.ExternalUtils;
import com.atlassian.jira.issue.AttachmentManager;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.IssueFactory;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.issue.MutableIssue;
import com.atlassian.jira.issue.index.IssueIndexManager;
import com.atlassian.jira.issue.index.IndexException;
import com.atlassian.jira.issue.comments.CommentManager;
import com.atlassian.jira.issue.context.GlobalIssueContext;
import com.atlassian.jira.issue.customfields.CustomFieldSearcher;
import com.atlassian.jira.issue.customfields.CustomFieldType;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.issuetype.IssueType;
import com.atlassian.jira.issue.priority.Priority;
import com.atlassian.jira.issue.status.Status;
import com.atlassian.jira.issue.watchers.WatcherManager;
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.project.version.Version;
import com.atlassian.jira.project.version.VersionManager;
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.jira.web.action.admin.customfields.CreateCustomField;
import com.atlassian.jira.web.util.AttachmentException;
import com.atlassian.jira.workflow.WorkflowManager;
import com.atlassian.jira.workflow.JiraWorkflow;
import com.atlassian.jira.workflow.WorkflowTransitionUtil;
import com.atlassian.jira.workflow.WorkflowTransitionUtilImpl;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.util.JiraUtils;
import com.atlassian.jira.util.ErrorCollection;
import com.atlassian.core.util.map.EasyMap;
import com.atlassian.core.ofbiz.CoreFactory;
import com.opensymphony.user.User;
import com.opensymphony.workflow.loader.StepDescriptor;
import com.opensymphony.workflow.loader.ActionDescriptor;
import com.specificgroup.jira.plugin.acpro.Constants;
import com.specificgroup.jira.plugin.acpro.sync.Sync;
import com.specificgroup.jira.plugin.acpro.sync.Event;
import com.specificgroup.jira.plugin.acpro.exception.AcproException;
import com.specificgroup.jira.plugin.acpro.mapping.JiraIssueType;
import com.specificgroup.jira.plugin.acpro.mapping.JiraPriority;
import com.specificgroup.jira.plugin.acpro.mapping.JiraProjectVersion;
import com.specificgroup.jira.plugin.acpro.mapping.JiraStatus;
import com.specificgroup.jira.plugin.acpro.mapping.JiraUser;
import com.specificgroup.jira.plugin.acpro.model.DownloadedAttachment;
import com.specificgroup.jira.plugin.acpro.model.Settings;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraAttachmentData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraCommentData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraImportData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraIssueData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraProject;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraProjectComponent;
import com.specificgroup.jira.plugin.acpro.service.JiraAcproPluginJiraManager;
import com.specificgroup.jira.plugin.acpro.util.ObjectsBuilder;
import org.apache.log4j.Logger;
import org.ofbiz.core.entity.GenericEntityException;
import org.ofbiz.core.entity.GenericValue;
import org.ofbiz.core.entity.GenericDelegator;

import java.io.File;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.Comparator;

/**
 * @author Ivan Holub
 */
public class JiraAcproPluginJiraManagerImpl implements JiraAcproPluginJiraManager {

    private Logger log = Logger.getLogger(getClass());

    private ConstantsManager constantsManager;

    private ProjectManager projectManager;

    private ProjectComponentManager projectComponentManager;

    private VersionManager versionManager;

    private UserManager userManager;

    private IssueFactory issueFactory;

    private IssueManager issueManager;

    private WatcherManager watcherManager;
    
    private AttachmentManager attachmentManager;

    private CommentManager commentManager;

    private CustomFieldManager customFieldManager;

    private ExternalUtils externalUtils;

    private WorkflowManager workflowManager;

    private GenericDelegator genericDelegator;

    private IssueIndexManager indexManager;

    public JiraAcproPluginJiraManagerImpl(ConstantsManager constantsManager, ProjectManager projectManager, ProjectComponentManager projectComponentManager, VersionManager versionManager, UserManager userManager, IssueFactory issueFactory, IssueManager issueManager, WatcherManager watcherManager, AttachmentManager attachmentManager, CommentManager commentManager, CustomFieldManager customFieldManager, ExternalUtils externalUtils, WorkflowManager workflowManager, GenericDelegator genericDelegator, IssueIndexManager indexManager) {
        this.constantsManager = constantsManager;
        this.projectManager = projectManager;
        this.projectComponentManager = projectComponentManager;
        this.versionManager = versionManager;
        this.userManager = userManager;
        this.issueFactory = issueFactory;
        this.issueManager = issueManager;
        this.watcherManager = watcherManager;
        this.attachmentManager = attachmentManager;
        this.commentManager = commentManager;
        this.customFieldManager = customFieldManager;
        this.externalUtils = externalUtils;
        this.workflowManager = workflowManager;
        this.genericDelegator = genericDelegator;
        this.indexManager = indexManager;
    }

    public List<JiraStatus> getJiraStatuses() {
        Collection<Status> list = constantsManager.getStatusObjects();
        List<JiraStatus> retValue = new ArrayList<JiraStatus>();
        for (Status it: list) {
            JiraStatus item = ObjectsBuilder.buildJiraStatus(it);
            retValue.add(item);
        }
        //sort by id
        Collections.sort(retValue, new Comparator<JiraStatus>() {
            public int compare(JiraStatus o1, JiraStatus o2) {
                Long id1 = Long.valueOf(o1.getId());
                Long id2 = Long.valueOf(o2.getId());
                return id1.compareTo(id2);
            }
        });
        return retValue;
    }

    public Map<String, JiraStatus> getJiraStatusesMap() {
        Collection<Status> list = constantsManager.getStatusObjects();
        Map<String, JiraStatus> retValue = new HashMap<String, JiraStatus>();
        for (Status it: list) {
            JiraStatus item = ObjectsBuilder.buildJiraStatus(it);
            retValue.put(item.getId(), item);
        }
        return retValue;
    }

    public List<JiraProject> getJiraProjects() {
        List<JiraProject> retValue = new ArrayList<JiraProject>();
        List<Project> projects = projectManager.getProjectObjects();
        for (Project p: projects) {
            JiraProject item = new JiraProject();
            item.setKey(p.getKey());
            item.setName(p.getName());
            item.setId(p.getId());
            retValue.add(item);
        }
        return retValue;
    }

    public List<JiraProjectComponent> getJiraProjectComponents(Long projectId) {
        List<JiraProjectComponent> retValue = new ArrayList<JiraProjectComponent>();
        Collection<ProjectComponent> projectComponents = projectComponentManager.findAllForProject(projectId);
        for (ProjectComponent c: projectComponents) {
            JiraProjectComponent item = new JiraProjectComponent();
            item.setId(c.getId());
            item.setName(c.getName());
            retValue.add(item);
        }
        //sort by id
        Collections.sort(retValue, new Comparator<JiraProjectComponent>() {
            public int compare(JiraProjectComponent o1, JiraProjectComponent o2) {
                Long id1 = o1.getId();
                Long id2 = o2.getId();
                return id1.compareTo(id2);
            }
        });
        return retValue;
    }

    public List<JiraIssueType> getJiraIssueTypes() {
        Collection<IssueType> issueTypes = constantsManager.getAllIssueTypeObjects();
        List<JiraIssueType> retValue = new ArrayList<JiraIssueType>();
        for (IssueType it: issueTypes) {
            JiraIssueType jiraIssueType = ObjectsBuilder.buildJiraIssueType(it);
            retValue.add(jiraIssueType);
        }
        //sort by id
        Collections.sort(retValue, new Comparator<JiraIssueType>() {
            public int compare(JiraIssueType o1, JiraIssueType o2) {
                Long id1 = Long.valueOf(o1.getId());
                Long id2 = Long.valueOf(o2.getId());
                return id1.compareTo(id2);
            }
        });
        return retValue;
    }

    public Map<String, JiraIssueType> getJiraIssueTypesMap() {
        Collection<IssueType> issueTypes = constantsManager.getAllIssueTypeObjects();
        Map<String, JiraIssueType> retValue = new HashMap<String, JiraIssueType>();
        for (IssueType item: issueTypes) {
            JiraIssueType jiraIssueType = ObjectsBuilder.buildJiraIssueType(item);
            retValue.put(jiraIssueType.getId(), jiraIssueType);
        }
        return retValue;
    }

    public List<JiraPriority> getJiraPriorities() {
        Collection<Priority> list = constantsManager.getPriorityObjects();
        List<JiraPriority> retValue = new ArrayList<JiraPriority>();
        for (Priority it: list) {
            JiraPriority item = ObjectsBuilder.buildJiraPriority(it);
            retValue.add(item);
        }
        //sort by id
        Collections.sort(retValue, new Comparator<JiraPriority>() {
            public int compare(JiraPriority o1, JiraPriority o2) {
                Long id1 = Long.valueOf(o1.getId());
                Long id2 = Long.valueOf(o2.getId());
                return id1.compareTo(id2);
            }
        });
        return retValue;
    }

    public Map<String, JiraPriority> getJiraPrioritiesMap() {
        Collection<Priority> list = constantsManager.getPriorityObjects();
        Map<String, JiraPriority> retValue = new HashMap<String, JiraPriority>();
        for (Priority it: list) {
            JiraPriority item = ObjectsBuilder.buildJiraPriority(it);
            retValue.put(item.getId(), item);
        }
        return retValue;
    }

    public List<JiraProjectVersion> getJiraProjectVersions(Long projectId) {
        List<JiraProjectVersion> retValue = new ArrayList<JiraProjectVersion>();
        List<Version> versions = versionManager.getVersions(projectId);
        for (Version v: versions) {
            JiraProjectVersion pv = ObjectsBuilder.buildJiraProjectVersion(v);
            retValue.add(pv);
        }
        //sort by id
        Collections.sort(retValue, new Comparator<JiraProjectVersion>() {
            public int compare(JiraProjectVersion o1, JiraProjectVersion o2) {
                Long id1 = o1.getId();
                Long id2 = o2.getId();
                return id1.compareTo(id2);
            }
        });
        return retValue;
    }

    public Map<Long, JiraProjectVersion> getJiraProjectVersionsMap(Long projectId) {
        Map<Long, JiraProjectVersion> retValue = new HashMap<Long, JiraProjectVersion>();
        List<Version> versions = versionManager.getVersions(projectId);
        for (Version v: versions) {
            JiraProjectVersion pv = ObjectsBuilder.buildJiraProjectVersion(v);
            retValue.put(pv.getId(), pv);
        }
        return retValue;
    }

    public List<JiraUser> getAllJiraUsers() {
        Set<User> users = userManager.getAllUsers();
        List<JiraUser> retValue = new ArrayList<JiraUser>();
        for (User user: users) {
            JiraUser jiraUser = ObjectsBuilder.buildJiraUser(user);
            retValue.add(jiraUser);
        }
        //sort by user name
        Collections.sort(retValue, new Comparator<JiraUser>() {
            public int compare(JiraUser o1, JiraUser o2) {
                String name1 = o1.getUserName();
                String name2 = o2.getUserName();
                return name1.compareTo(name2);
            }
        });
        return retValue;
    }

    public Map<String, JiraUser> getJiraUsersMap() {
        Set<User> users = userManager.getAllUsers();
        Map<String, JiraUser> retValue = new HashMap<String, JiraUser>();
        for (User user: users) {
            JiraUser jiraUser = ObjectsBuilder.buildJiraUser(user);
            retValue.put(user.getName(), jiraUser);
        }
        return retValue;
    }

    public void createIssue(Sync sync, JiraImportData jiraImportData, JiraIssueData data, Map<String, Map<String, DownloadedAttachment>> downloadedAttachments) {
        Event event = new Event(Event.MESSAGE);
        event.setMessage("Creating Jira Issue from Acpro Item: " + data.getAcproItemKey() + ".");
        sync.addEvent(event);

        //TODO validate not optional fields
        String acproItemKey = data.getAcproItemKey();
        MutableIssue issue = issueFactory.getIssue();
        issue.setProjectId(jiraImportData.getProjectId());
        User assignee = data.getAssignee();
        if (assignee != null) {
            issue.setAssignee(assignee);
        }
        issue.setSummary(data.getSummary());
        issue.setDescription(data.getDescription());
        issue.setIssueTypeId(data.getIssueTypeId());
        issue.setPriorityId(data.getPriorityId());
        issue.setReporter(data.getReporter());
        Collection<GenericValue> projectComponents = jiraImportData.getProjectComponentsGeneric();
        if (projectComponents != null) {
            issue.setComponents(projectComponents);
        }
        Long estimate = data.getEstimate();
        if (estimate != null) {
            issue.setOriginalEstimate(estimate);
        }
        issue.setCustomFieldValue(jiraImportData.getAcproItemNumberCustomField(), acproItemKey);
        issue.setCreated(new Timestamp(data.getDateCreated().getTime()));
        Date dueDate = data.getDueDate();
        if (dueDate != null) {
            issue.setDueDate(new Timestamp(dueDate.getTime()));
        }
        Version affectedVersion = data.getAffectedVersion();
        if (affectedVersion != null) {
            issue.setAffectedVersions(Arrays.asList(affectedVersion));
        }
        Version fixVersion = data.getFixVersion();
        if (fixVersion != null) {
            issue.setFixVersions(Arrays.asList(fixVersion));
        }
        GenericValue retValue;
        try {
            retValue = issueManager.createIssue(data.getIssueCreator(), issue);
        } catch (CreateException e) {
            log.error("Error", e);
            throw new RuntimeException(e);
        }

        updateIssueStatus(issue, data.getStatusId());

        List<User> watchers = data.getWatchers();
        for (User user: watchers) {
            watcherManager.startWatching(user, retValue);
        }
        List<JiraCommentData> comments = data.getComments();
        if (comments != null) {
            for (JiraCommentData comment: comments) {
                createJiraComment(issue, comment);
            }
        }
        List<JiraAttachmentData> attachments = data.getAttachments();
        if (attachments != null) {
            Map<String, DownloadedAttachment> filesMap = downloadedAttachments.get(acproItemKey);
            for (JiraAttachmentData attachment: attachments) {
                createJiraAttachment(issue, attachment, filesMap);
            }
        }

        event = new Event(Event.ISSUE);
        sync.addEvent(event);
    }

    private void updateIssueStatus(MutableIssue issue, String statusId) {

        Status status = constantsManager.getStatusObject(statusId);
        JiraWorkflow workflow = workflowManager.getWorkflow(issue);
        StepDescriptor stepDescriptor = workflow.getLinkedStep(status.getGenericValue());
        if (stepDescriptor == null) {
            //must not occur, is checked before import
            throw new RuntimeException("Status is not assigned to any step, status: " + status.getName());
        }

        final Integer stepId = stepDescriptor.getId();
        final String statusName = status.getName();

        //update jiraissue.issuestatus in database
        issue.setStatusId(statusId);
        try {
            issue.getGenericValue().store();
        } catch (GenericEntityException e) {
            log.error("Error", e);
            //must not occur
            throw new RuntimeException(e);
        }

        Long workflowId = issue.getWorkflowId();
        // retrieve the wfCurrentStep for this issue and change it
        try {
            Collection wfCurrentStepCollection = genericDelegator.findByAnd("OSCurrentStep", EasyMap.build("entryId", workflowId));
            if (wfCurrentStepCollection.size() == 0) {
                //must not occur
                String message = "Current step is not found in database, entryId: " + workflowId;
                log.error(message);
                throw new RuntimeException(message);
            }
            final GenericValue wfCurrentStep = (GenericValue) wfCurrentStepCollection.iterator().next();
            //update os_currentstep.STEP_ID in database
            wfCurrentStep.set("stepId", stepId);
            //update os_currentstep.STATUS in database
            wfCurrentStep.set("status", statusName);
            wfCurrentStep.store();

            //without reindex old status value (OPEN) is displayed in issues table and search by new status value doesn't work,
            //so reindex is required
            indexManager.reIndex(issue);

        } catch (GenericEntityException e) {
            log.error("Error", e);
            //must not occur
            throw new RuntimeException(e);
        } catch (IndexException e) {
            log.error("Error", e);
            //must not occur
            throw new RuntimeException(e);
        }
    }

    private void createJiraAttachment(Issue issue, JiraAttachmentData attachment, Map<String, DownloadedAttachment> filesMap) {
        String filename = attachment.getFilename();
        DownloadedAttachment da = filesMap.get(filename);
        File file = da.getFile();
        String contentType = da.getContentType();
        Date createdTime = attachment.getCreatedTime();
        try {
            attachmentManager.createAttachment(file, filename, contentType, null, issue.getGenericValue(), null, new Timestamp(createdTime.getTime()));
        } catch (AttachmentException e) {
            //must not happen
            log.error("Error", e);
            throw new RuntimeException(e);
        } catch (GenericEntityException e) {
            //must not happen
            log.error("Error", e);
            throw new RuntimeException(e);
        }
    }

    private void createJiraComment(Issue issue, JiraCommentData comment) {
        String body = comment.getBody();
        Date dateCreated = comment.getDateCreated();
        String authorUserName = comment.getAuthorUserName();
        commentManager.create(issue, authorUserName, null, body, null, null, dateCreated, null, false, false);
    }

    public void createAcproItemNumberCustomField() {
        CustomFieldType customFieldType = customFieldManager.getCustomFieldType(CreateCustomField.FIELD_TYPE_PREFIX +
                Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_TYPE);
        CustomFieldSearcher customFieldSearcher = customFieldManager.getCustomFieldSearcher(CreateCustomField.FIELD_TYPE_PREFIX +
                Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_SEARCHER);
        try {
            List a = new ArrayList();
            a.add(null);
            CustomField cf = customFieldManager.createCustomField(Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_NAME,
                Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_DESCRIPTION,
                customFieldType,
                customFieldSearcher,
                Arrays.asList(GlobalIssueContext.getInstance()),
                a);
            externalUtils.associateCustomFieldWithScreen(cf, null);
        } catch (GenericEntityException e) {
            //must not occur
            log.error("Error", e);
            throw new RuntimeException(e);
        }
    }

    public boolean isAcproItemNumberCustomFieldExists() {
        CustomField cf = customFieldManager.getCustomFieldObjectByName(Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_NAME);
        boolean retValue = cf != null;
        return retValue;
    }

    public void validateJiraStatus(String statusId, Long jiraProjectId, String issueTypeId) throws AcproException {
        JiraWorkflow workflow = workflowManager.getWorkflow(jiraProjectId, issueTypeId);
        Status status = constantsManager.getStatusObject(statusId);
        StepDescriptor stepDescriptor = workflow.getLinkedStep(status.getGenericValue());
        if (stepDescriptor == null) {
            throw new AcproException("Status is not assigned to any step, status: " + status.getName() + ", workflow: " + workflow.getName());
        }
    }

}
